6.2 Konstruktoren in abgeleiteten Klassen
 
Bei der Erzeugung des Objekts einer Subklasse gelten dieselben Regeln wie beim Erzeugen des Objekts einer Basisklasse:
|
Es wird generell ein Konstruktor aufgerufen. |
|
Die Subklassenkonstruktoren dürfen überladen werden. |
Es gibt jedoch eine Besonderheit, die im Zusammenhang mit den Konstruktoren beachtet werden muss:
| Konstruktoren werden grundsätzlich nicht von der Basisklasse an die Subklasse vererbt.
|
Daher müssen alle Konstruktoren, die in einer abgeleiteten Klasse benötigt werden, definiert werden. Das gilt auch für den statischen Initialisierer.
6.2.1 Die Konstruktoren der Klasse »GraphicCircle«
 
Abgesehen vom impliziten, parameterlosen Standardkonstruktor
ist die Klasse GraphicCircle noch ohne Konstruktor. Um dem Anspruch zu genügen, einem Circle-Objekt auch hinsichtlich der Instanziierbarkeit gleichwertig zu sein, benötigen wir insgesamt vier Konstruktoren, die in der Lage sind, entweder den Radius oder den Radius samt der beiden Mittelpunktkoordinaten entgegenzunehmen. Außerdem müssen wir berücksichtigen, dass Objekte vom Typ GraphicCircle gleichzeitig Objekte vom Typ Circle sind. Die logische Konsequenz ist, den Objektzähler mit jedem neuen GraphicCircle-Objekt zu erhöhen.
Mit diesen Vorgaben, die identisch mit denen in der Basisklasse sind, sieht der erste und, wie Sie noch sehen werden, etwas blauäugige Entwurf der Erstellungsroutinen in der Klasse GraphicCircle zunächst folgendermaßen aus:
| public class GraphicCircle : Circle {
|
| public GraphicCircle() {
|
| countCircles++;
|
| }
|
| public GraphicCircle(double Radius):this() {
|
| this.Radius = Radius;
|
| }
|
| public GraphicCircle(double Radius, int X, int Y):this(Radius) {
|
| this.center.X = X;
|
| this.center.Y = Y;
|
| }
|
| public GraphicCircle(double Radius, Point pt):this(Radius) {
|
| this.radius = Radius;
|
| this.center = pt;
|
| }
|
| }
|
Der Versuch, diesen Programmcode zu kompilieren, endet jedoch in einem Fiasko, denn die Felder radius, center und countCircles kann der C#-Compiler nicht erkennen und verweigert deswegen den Zugriff. Der Grund hierfür ist recht einfach: Diese Felder sind in der Basisklasse Circle private deklariert, und private Member sind grundsätzlich nur in der sie deklarierenden Klasse bekannt. Obwohl aus objektorientierter Sicht ein Objekt vom Typ GraphicCircle auch gleichzeitig als ein Objekt vom Typ Circle angesehen wird, kann die strikte Kapselung einer privaten Variablen durch die Vererbung nicht aufgebrochen werden. Nur der Code in der Klasse Circle hat Zugriff auf die privaten Klassenmitglieder.
6.2.2 Der Zugriffsmodifizierer »protected«
 
Einen Ausweg aus diesem Dilemma, ein Klassenmitglied einerseits gegen den unbefugten Zugriff von außen zu schützen und andererseits einer abgeleiteten Klasse zu vererben, bietet ein Zugriffsmodifizierer, der im Zusammenhang mit der Implementierungsvererbung eine tragende Rolle hat: protected.
| Member, die protected deklariert sind, verhalten sich ähnlich wie private deklarierte: Sie verhindern den unzulässigen Zugriff von außerhalb, garantieren andererseits jedoch, dass aus einer abgeleiteten Klasse darauf zugegriffen werden kann.
|
Diese Erkenntnis führt zu einem Umdenken bei der Implementierung einer Klasse: Muss davon ausgegangen werden, dass die Klasse als Basisklasse ihre Dienste zur Verfügung stellt, sind alle privaten Member, die einer abgeleiteten Klasse zur Verfügung stehen sollen, protected zu deklarieren. Daher müssen wir in der Klasse Circle noch folgende Änderungen vornehmen:
| // Änderung der Zugriffsmodifizierer der privaten Felder
|
| // in der Klasse Circle
|
| protected Point center = new Point();
|
| protected double radius = 0;
|
| protected static int countCircles;
|
Erst jetzt ist die Klasse Circle tatsächlich vererbungsfähig, und der C#-Compiler wird keinen Fehler mehr melden.
6.2.3 Konstruktorverkettung
 
Wir wollen nun die Implementierung in Main testen, indem wir ein Objekt des Typs GraphicCircle erzeugen und uns den Stand des Objektzählers, der aus der Circle-Klasse geerbt wird, an der Konsole ausgeben lassen. Der Code dazu lautet:
| static void Main(string[] args) {
|
| GraphicCircle gc = new GraphicCircle();
|
| Console.WriteLine("Anzahl der Kreise = {0}", GraphicCircle.CountCircles);
|
| }
|
Völlig unerwartet werden wir mit einer Situation konfrontiert, die wir nicht vorhergesehen haben. Mit
wird uns suggeriert, wir hätten zwei Kreisobjekte erzeugt, obwohl wir doch tatsächlich nur einmal den new-Operator benutzt haben und sich folgerichtig auch nur ein konkretes Objekt im Speicher befinden kann.
Das Ergebnis ist falsch und beruht auf der bisher noch nicht berücksichtigten Aufrufverkettung zwischen den Sub- und den Basisklassenkonstruktoren. Konstruktoren werden bekanntlich nicht vererbt und müssen deshalb – falls erforderlich – in jeder abgeleiteten Klasse neu definiert werden. Dennoch kommt den Konstruktoren einer Basisklasse eine entscheidende Bedeutung zu.
| Bei der Initialisierung des Objekts vom Typ einer abgeleiteten Klasse wird in jedem Fall zuerst ein Basisklassenkonstruktor aufgerufen. Es kommt zu einer Top-down-Verkettung der Konstruktoren, angefangen bei der obersten Basisklasse bis hinunter zu der Klasse, deren Konstruktor ausgeführt werden muss.
|
Die Verkettung der Konstruktoraufrufe dient dazu, zunächst die geerbten Komponenten der Basisklasse zu initialisieren. Erst danach wird der Konstruktor der Subklasse ausgeführt, der eigene Initialisierungen vornehmen kann und gegebenenfalls auch die Vorinitialisierung der geerbten Komponenten an die Bedürfnisse der abgeleiteten Klasse anpasst. Standardmäßig wird implizit immer zuerst der parameterlose Konstruktor der Basisklasse aufgerufen.
Wir wollen uns das an einem einfachen Beispiel verdeutlichen.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 6\Konstruktorverkettung
|
| // --------------------------------------------------------------
|
|
|
| // ----- Basisklasse -----
|
| class BaseClass {
|
| //Standardkonstruktor
|
| public BaseClass() {
|
| Console.WriteLine("Konstruktor in 'BaseClass'");
|
| }
|
| }
|
| // ----- SubClass1 leitet BaseClass ab -----
|
| class SubClass1 : BaseClass {
|
| //Standardkonstruktor
|
| public SubClass1() {
|
| Console.WriteLine("Konstruktor in 'SubClass1'");
|
| }
|
| }
|
| // ----- SubClass2 leitet SubClass1 ab -----
|
| class SubClass2 : SubClass1 {
|
| //Standardkonstruktor
|
| public SubClass2() {
|
| Console.WriteLine("Konstruktor (parameterlos) in 'SubClass2'");
|
| }
|
| // parameterbehafteter Konstruktor
|
| public SubClass2(int x) {
|
| Console.WriteLine("Konstruktor (parametrisiert) in 'SubClass2'");
|
| }
|
| }
|
Die Klasse BaseClass dient als Basisklasse der Klasse SubClass1, die ihrerseits wiederum Basisklasse der Klasse SubClass2 ist. In allen Klassen ist der parameterlose Konstruktor implementiert, in SubClass2 zusätzlich noch ein parametrisierter. Um die Verkettung der Konstruktoren zu testen, werden zwei Objekte des Typs SubClass2 erzeugt: einmal durch Aufruf des parameterlosen Konstruktors, im zweiten Fall mit dem Aufruf des parametrisierten.
| class Class1 {
|
| static void Main(string[] args) {
|
| SubClass2 obj = new SubClass2();
|
| Console.WriteLine("---------------------------");
|
| SubClass2 obj2 = new SubClass2(2);
|
| Console.ReadLine();
|
| }
|
| }
|
Die Ausgabe an der Konsole bestätigt die Aussage, dass implizit zuerst der parameterlose Basisklassenkonstruktor aufgerufen wird, danach der Konstruktor des eigentlichen Objekts.
| Konstruktor in 'BaseClass'
|
| Konstruktor in 'SubClass1'
|
| Konstruktor (parameterlos) in 'SubClass2'
|
| -----------------------------
|
| Konstruktor in 'BaseClass'
|
| Konstruktor in 'SubClass1'
|
| Konstruktor (parametrisiert) in 'SubClass2'
|
Dabei ist es bedeutungslos, ob das Objekt einer abgeleiteten Klasse mit oder ohne Argumente instanziiert wird, denn der implizite Aufruf landet immer beim parameterlosen Konstruktor der Basisklasse. Wenn wir präzise sein wollen, müssen wir sogar sagen, dass die Aufrufverkettung nicht in BaseClass beginnt, sondern tatsächlich in Object, da diese Klasse oberste Basisklasse aller .NET-Klassen ist und als einzige Klasse selbst keine Basisklasse besitzt.
Die Konstruktorverkettung hat maßgeblichen Einfluss auf die Modellierung einer Klasse, die parametrisierte Konstruktoren enthält. Eine konstruktorlose Klasse enthält grundsätzlich immer den impliziten parameterlosen Konstruktor. Ergänzt man aber eine Klassendefinition um einen parametrisierten, existiert der implizite, parameterlose nicht mehr.
 Hier klicken, um das Bild zu vergrößern
Abbildung 6.3 Implizite Verkettung der Konstruktoraufrufe in einer Objekthierarchie
Wird das Objekt einer ableitenden Klasse erzeugt, kommt es – wie wir oben gesehen haben – zum Aufruf des parameterlosen Konstruktors der Basisklasse. Sie sollten sich dieses Effekts bewusst sein, wenn Sie eine ableitbare Klasse entwickeln und parametrisierte Konstruktoren hinzufügen. Im Bedarfsfall ist der parameterlose Konstruktor zu implementieren – selbst dann, wenn er keinen Programmcode enthält.
Die Konstruktorverkettung steuern
Nun erklärt sich auch das scheinbar unsinnige Ergebnis des Objektzählers im vorhergehenden Abschnitt, der bei der Instanziierung eines Objekts vom Typ GraphicCircle behauptete, zwei Kreisobjekte würden vorliegen, obwohl es nachweislich nur ein einziges sein konnte. Durch die Konstruktorverkettung wird – so wie es die noch unveränderte Implementierung der Konstruktoren vorgibt – zunächst der parameterlose Konstruktor der Basisklasse Circle aufgerufen, danach der der Klasse GraphicCircle. In beiden wird der Objektzähler erhöht, was letztendlich zu einem falschem Zählerstand führt. Die Ursache des Problems ist die Duplizität der Implementierung der beiden parameterlosen Konstruktoren, nämlich in Circle
| public Circle() {
|
| countCircles++;
|
| }
|
und in der von Circle abgeleiteten Klasse GraphicCircle:
| public GraphicCircle() {
|
| countCircles++;
|
| }
|
Betrachten wir auch noch einmal die Implementierung der anderen Konstruktoren in GraphicCircle:
| public GraphicCircle(double Radius):this(){...}
|
| public GraphicCircle(double Radius, int X, int Y):this(Radius){...}
|
| public GraphicCircle(double Radius, Point pt):this(Radius){...}
|
Alle Aufrufe parametrisierter Konstruktoren werden derzeit mit this an den parameterlosen Konstruktor weitergeleitet, der dann seinerseits implizit den parameterlosen der Basisklasse Circle aufruft. Wir können unser Problem am einfachsten lösen, wenn wir anstelle des this-Aufrufs den Aufruf unter Weiterleitung aller Parameter direkt an den Konstruktor der Basisklasse delegieren, dessen Parameterliste identisch ist.
Um eine Methode in der Basisklasse auszuführen – und als eine solche werden auch die Konstruktoren angesehen –, bietet C# das Schlüsselwort base an, mit dem innerhalb einer abgeleiteten Klasse auf Mitglieder der Basisklasse zugegriffen werden kann. Handelt es sich um einen Konstruktor, müssen hinter dem Schlüsselwort die Argumente in runden Klammern spezifiziert werden, die der aufgerufene Konstruktor entgegennehmen soll. Aufgrund der Anzahl und der Typen der Argumente ist damit auch der aufzurufende Basisklassenkonstruktor festgelegt. Da das objektorientierte Paradigma vorschreibt, dass aus einer Subklasse heraus mittels Aufrufverkettung immer zuerst ein Konstruktor der Basisklasse ausgeführt werden muss, haben wir die implizite Verkettung durch eine explizite ersetzt und die Steuerung selbst übernommen: Es kommt zu keinem weiteren impliziten Aufruf mehr, auch nicht an den parameterlosen Basisklassenkonstruktor.
Sehen wir uns nun abschließend die überarbeitete Fassung der GraphicCircle-Konstruktoren an:
| public GraphicCircle() {}
|
| public GraphicCircle(double Radius):base(Radius) {}
|
| public GraphicCircle(double Radius, int X, int Y) : base(Radius, X, Y) {}
|
| public GraphicCircle(double Radius, Point pt) : base(Radius, pt) {}
|
Beim parameterlosen Konstruktor können wir auf die base-Anweisung verzichten, weil der Aufruf auch ohne diese Angabe an den parameterlosen Basisklassenkonstruktor weitergeleitet wird.
Schreiben wir jetzt eine Testroutine, z.B.:
| GraphicCircle gc = new GraphicCircle();
|
| Console.WriteLine("Anzahl der Kreise = {0}",
|
| GraphicCircle.CountCircles);
|
wird die Ausgabe des Objektzählers tatsächlich den korrekten Stand wiedergeben.
Der Zugriff auf Entitäten der Basisklasse mit »base«
Mit base kann auf alle Mitglieder der direkten Basisklasse Bezug genommen werden, solange sie nicht private deklariert sind. Dabei gilt, dass die Methode der Basisklasse, auf die zugegriffen wird, durchaus eine von dieser Klasse selbst geerbte Methode sein kann, also aus Sicht der base-implementierenden Subklasse aus einer indirekten Basisklasse stammt, beispielsweise:
| class BaseClass {
|
| public void TestMethod() {
|
| Console.WriteLine("In 'BaseClass.TestMethod()'");
|
| }
|
| }
|
| class SubClass1 : BaseClass {}
|
| class SubClass2 : SubClass1 {
|
| public void BaseTest() {
|
| base.TestMethod();
|
| }
|
| }
|
Ein umgeleiteter Aufruf an eine indirekte Basisklasse mit
| // unzulässiger Aufruf
|
| base.base.TestMethod();
|
ist nicht gestattet.
Handelt es sich bei der über base aufgerufenen Methode um eine parametrisierte, müssen den Parametern die entsprechenden Argumente übergeben werden.
| base ist eine implizite Referenz und als solche an eine konkrete Instanz gebunden. Das bedeutet auch, dass dieses Schlüsselwort nicht zum Aufruf von statischen Methoden verwendet werden kann.
|
6.2.4 Destruktor-Verkettung
 
Konstruktoren dienen der Initialisierung eines Objekts, der Destruktor wird aufgerufen, wenn der Garbage Collector auf ein Objekt stößt, das im Programmcode nicht weiter benutzt wird.
Ein verketteter Aufruf in einer Klassenhierarchie findet auch statt, wenn der Garbage Collector bei seinen Aufräumarbeiten auf den Destruktor einer Subklasse stößt. Der Ablauf der Verkettung spielt sich allerdings in umgekehrter Reihenfolge ab: Zuerst werden die Aufräumarbeiten in der Subklasse geleistet, danach die in der direkten Basisklasse. Diese Bottom-up-Verkettung wollen wir uns an einem Beispiel ansehen.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 6\Destruktorverkettung
|
| //--------------------------------------------------------------
|
| class Program {
|
| static void Main(string[] args) {
|
| SubClass2 obj = new SubClass2();
|
| obj = null;
|
| GC.Collect();
|
| Console.ReadLine();
|
| }
|
| }
|
| class BaseClass {
|
| ~BaseClass() {
|
| Console.WriteLine("Anweisungen in 'BaseClass'");
|
| Console.WriteLine("Destruktor in 'BaseClass'");
|
| }
|
| }
|
| class SubClass1 : BaseClass {
|
| ~SubClass1() {
|
| Console.WriteLine("Anweisungen in 'SubClass1'");
|
| Console.WriteLine("Destruktor in 'SubClass1'");
|
| }
|
| }
|
| class SubClass2 : SubClass1 {
|
| ~SubClass2() {
|
| Console.WriteLine("Anweisungen in 'SubClass2'");
|
| Console.WriteLine("Destruktor in 'SubClass2'");
|
| }
|
| }
|
Wieder sind es die drei Klassen BaseClass, SubClass1 und SubClass2, die miteinander in einer Vererbungsbeziehung stehen. Diesmal enthält jede der drei Klassen einen Destruktor, der jeweils zwei Anweisungen enthält, um Meldungen an der Konsole auszugeben.
In Main wird ein Objekt der abgeleiteten Klasse SubClass2 erzeugt und sofort wieder mit
freigegeben. Damit wir nicht nur beim endgültigen Schließen des Konsolenfensters für einen kurzen Moment die Meldungen zu sehen bekommen, wird der Garbage Collector mit der statischen Methode Collect der Klasse GC angestoßen, um das freigegebene Objekt endgültig zu zerstören. An der Konsole sehen wir die Aussage bestätigt:
| Anweisungen in 'SubClass2'
|
| Destruktor in 'SubClass2'
|
| Anweisungen in 'SubClass1'
|
| Destruktor in 'SubClass1'
|
| Anweisungen in 'BaseClass'
|
| Destruktor in 'BaseClass'
|
Es fällt auch noch ein zweiter, wesentlicher Unterschied im Vergleich zu den Konstruktoren auf: Der Sprung zu dem Destruktor der Basisklasse erfolgt erst nach der Abarbeitung des letzten Befehls im Anweisungsblock.
6.2.5 Der Stand des Projekts »CircleApplication«
 
Fassen wir an dieser Stelle noch einmal alle Ergänzungen und Änderungen des Projekts CircleApplication zusammen.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 6\CircleApplication_4
|
| // --------------------------------------------------------------
|
| public class Circle : IDisposable {
|
| // ---------- statische Felder ----------
|
| protected static int countCircles;
|
| // ---------- Instanzvariablen ----------
|
| protected Point center = new Point();
|
| protected double radius = 0;
|
| private bool counterReduced = false;
|
| ...
|
| }
|
| public class GraphicCircle : Circle {
|
| // Konstruktoren
|
| public GraphicCircle() {}
|
| public GraphicCircle(double Radius):base(Radius) {}
|
| public GraphicCircle(double Radius, int X, int Y):base(Radius, X, Y) {}
|
| public GraphicCircle(double Radius, Point pt):base(Radius, pt) {}
|
| // typspezifische Methode
|
| public void Draw() {
|
| Console.WriteLine("Der Kreis wird gezeichnet");
|
| }
|
| }
|
6.2.6 Zusammenfassung
 
|
Konstruktoren werden grundsätzlich nicht von der Basisklasse an die Subklasse vererbt. |
|
private deklarierte Mitglieder in einer Basisklasse sind in der abgeleiteten Klasse nicht sichtbar. Um das Prinzip der Kapselung nicht aufzubrechen und diese Klassenmitglieder zu vererben, muss das entsprechende Mitglied in der Basisklasse protected deklariert werden. |
|
Sub- und Basisklassenkonstruktor hängen in der Weise voneinander ab, dass zuerst der Basisklassenkonstruktor und danach der Konstruktor der abgeleiteten Klasse ausgeführt wird. |
|
Wenn nicht anders angegeben, wird in einer abgeleiteten Klasse implizit zuerst der parameterlose Standardkonstruktor der Basisklasse aufgerufen, bevor die erste Anweisung im Subklassenkonstruktor ausgeführt wird. |
|
Mit dem Schlüsselwort base kann die implizite Konstruktorverkettung zwischen zwei in einer Vererbungsbeziehung stehenden Klassen durch eine benutzerdefinierte ersetzt werden. |
|
Um einen bestimmten Basisklassenkonstruktor aufzurufen, müssen hinter base in runden Klammern die entsprechenden Parameter übergeben werden. |
|
Mit base kann auf alle Methoden und Eigenschaften der direkten Basisklasse Bezug genommen werden, solange sie nicht private deklariert sind. |
|
Stößt der Garbage Collector bei seinen Aufräumarbeiten im freizugebenden Objekt auf einen Destruktor, werden alle Destruktoren in der Vererbungslinie durchlaufen, beginnend mit dem des aktuellen Objekts. |
|
Der Aufruf des Basisklassendestruktors erfolgt erst, nachdem alle Anweisungen des Destruktors der Subklasse ausgeführt worden sind. |
|